﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using DotNet.DevOps.Tasks.Util;
using Microsoft.Build.Framework;

namespace DotNet.DevOps.Tasks.AppConfig
{
    public interface IAppSetting
    {
        String Key { get; set; }
        String Value { get; set; }
        string Files { get; set; }
        bool AddIfMissing { get; set; }
        int ExpectedMatches { get; set; }
    }

    /// <summary>
    /// Updates or adds app settings to your app.config / web.config file(s).
    /// If no matching key is found the default behaviour is to add a new key. This can be overridden by setting AddIfMissing. 
    /// Also only 1 occourence of the key is expteced by default. This can be overridden by setting ExpectedMatches.
    /// 
    /// Default behaviour:
    /// * Adds key if missing.
    /// * Expects 1 key match.
    /// 
    /// </summary>
    /// <example>
    /// import-module .\DotNet.DevOps.Tasks.dll
    /// 
    /// $task = New-Object DotNet.DevOps.Tasks.AppConfig.AppSetting -Property @{
    ///                                                           Key             = "DataSource"
    ///                                                           Value           = "TESTDB"
    ///                                                           Files           = "app.config"
    ///                                                         }
    /// $result = $task.Execute()
    /// </example>
    /// <example>
    /// <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    /// 
    ///  <Import Project="$(libs)\DotNet.DevOps.Tasks.Targets" />
    /// 
    ///  <Target Name="Build">
    /// 
    ///    <AppSetting Files="app.config" Key="DataSource" Value="ACCEPTTEST" />
    /// 
    ///  </Target>
    /// 
    /// </Project>
    /// </example>
    public class AppSetting : ITask, IAppSetting
    {
        public IBuildEngine BuildEngine { get; set; }
        public ITaskHost HostObject { get; set; }

        /// <summary>
        /// App setting Key to find.
        /// </summary>
        [Required]
        public String Key { get; set; }

        /// <summary>
        /// App setting Value to set.
        /// </summary>
        [Required]
        public String Value { get; set; }

        /// <summary>
        /// File(s) to update.
        /// </summary>
        [Required]
        public string Files { get; set; }

        /// <summary>
        /// Default true, set to false if you want the build to fail if the key is not allready present in the file.
        /// </summary>
        [Standard]
        public bool AddIfMissing { get; set; }

        /// <summary>
        /// Number of matches that should be found in the file. If the number do not match the number of actual entries in the file, the build will fail.
        /// </summary>
        [Standard]
        public int ExpectedMatches { get; set; }

        private ILog Log { get; set; }

        public AppSetting()
        {
            Init();
            Log = new MSBuildLogger(this);
        }

        public AppSetting(ILog logger)
        {
            Init();
            Log = logger;
        }

        private void Init()
        {
            AddIfMissing = true;
            ExpectedMatches = 1;
        }

        public bool Execute()
        {
            try
            {
                var filenames = Files.GetValidFilenames();

                filenames.ToList().ForEach(InsertOrUpdateAppSetting);
            }
            catch(XmlException xmlEx)
            {
                Log.LogError(new Exception(string.Format("There's a problem with your input file.\r\n {0}", xmlEx.Message)));
                return false;
            }
            catch (Exception ex)
            {
                Log.LogError(ex);
                return false;
            }

            return true;
        }

        private void InsertOrUpdateAppSetting(string filename)
        {
            var xDoc = XDocument.Load(filename);
            Log.LogInfo(string.Format("Modifying: {0}", filename));

            var matchingNodes = GetMatchingNodes(xDoc);

            ValidateNodesFound(matchingNodes.Count());

            if (matchingNodes.Count() > 0)
            {
                UpdateNodes(matchingNodes);
            }
            else
            {
                InsertNewNode(xDoc);
            }

            xDoc.Save(filename);
        }

        private IEnumerable<XElement> GetMatchingNodes(XDocument xDoc)
        {
            return xDoc.Root.DescendantNodes().OfType<XElement>()
                    .Where(e => e.Name.ToString().ToLower() == "appsettings")
                        .Elements()
                            .Where(e => e.Name.ToString().ToLower() == "add")
                            .Where(e => e.Attributes().Where(atr => atr.Name.ToString().ToLower() == "key" && atr.Value == Key)
                                .Count() > 0);
        }

        private void ValidateNodesFound(int matchingNodesFound)
        {
            CheckIfNewSettingCanBeAdded(matchingNodesFound);
            CheckIfTheresToFewIdenticalSettings(matchingNodesFound);
            CheckIfTheresToManyIdenticalSettings(matchingNodesFound);
        }

        private void CheckIfNewSettingCanBeAdded(int matchingNodesFound)
        {
            if (matchingNodesFound == 0 && AddIfMissing == false)
            {
                throw new Exception(string.Format("No match searching for key=\"{0}\". Setting will NOT be added as AddIfMissing is set to FALSE.", Key));
            }
        }

        private void CheckIfTheresToFewIdenticalSettings(int matchingNodesFound)
        {
            if (matchingNodesFound < ExpectedMatches && AddIfMissing == false)
            {
                throw new Exception(string.Format("Too few matches searching for key=\"{0}\"! {1} found, expected {2}.", Key, matchingNodesFound, ExpectedMatches));
            }
        }

        private void CheckIfTheresToManyIdenticalSettings(int matchingNodesFound)
        {
            if (matchingNodesFound > ExpectedMatches)
            {
                throw new Exception(string.Format("Too many matches searching for key=\"{0}\"! {1} found, expected {2}.", Key, matchingNodesFound, ExpectedMatches));
            }
        }

        private void UpdateNodes(IEnumerable<XElement> matchingNodes)
        {
            matchingNodes.ToList().ForEach(SetValue);
        }

        private void SetValue(XElement node)
        {
            var oldValue = node.Attributes().Where(atr => atr.Name.ToString().ToLower() == "value").First().Value;

            node.Attributes().Where(atr => atr.Name.ToString().ToLower() == "value").First().SetValue(Value);

            Log.LogInfo(string.Format("Updated \"{0}\": value=\"{1}\" -> value=\"{2}\"", Key, oldValue, Value));
        }

        private void InsertNewNode(XDocument xDoc)
        {
            xDoc.Root.DescendantNodes().OfType<XElement>()
                .Where(e => e.Name.ToString().ToLower() == "appsettings")
                    .ToList().ForEach(element => element.Add(new XElement("add", new XAttribute("key",Key), new XAttribute("value",Value))));

            Log.LogWarning(string.Format("Inserted \"{0}\": value=\"{1}\"", Key, Value));
        }

        public override string ToString()
        {
            return string.Format("AppSetting Key={0}, Value={1}, AddIfMissing={2}, ExpectedMatches={3}, Files={4}", Key,Value, AddIfMissing, ExpectedMatches, Files);
        }
    }
}
